在 Monolithic 架構下,經常會將程式碼放在同一個 Git Repository,包含:前端、後端等。不過在微服務架構下,因為不再將所有功能放在一起,而是由各個團隊負責實作、維運自己負責的服務,於是在程式碼管理上有兩大策略:PolyRepo 與 Monorepo。
補充:實務上採用 PolyRepo 還是 Monorepo 要看組織文化與團隊共識,並沒有絕對的好壞。
PolyRepo 是一種以 多個 Git Repository 來拆分、管理程式碼的策略。在微服務架構下,因為每個服務都有自己的生命週期、技術與團隊,將程式碼放在不同的 Git Repository 是目前主流的做法,也是最直覺拆分程式碼的策略。
下方是採用 PolyRepo 的優點:
當然,PolyRepo 也是有缺點的:
Monorepo 將程式碼集中於單一 Git Repository 進行管理,與 Monolithic 架構不同的地方在於,Monorepo 是以拆分服務的方式進行管理,並非混合成同一個服務。近年來,這類型的管理方式也逐漸流行起來。
下方是採用 Monorepo 的優點:
但 Monorepo 也是有缺點的:
補充:本篇文章將會聚焦在 Monorepo 的管理策略上。
Nx 是一套強大的 Monorepo 管理工具,支援多項熱門框架,包含:React、Angular、NestJS 等。組織可以透過 Nx 來定義程式碼的 邊界(Boundary),確保程式碼的依賴關係與品質,甚至還可以利用 Nx 的程式碼產生器來輔助產生基礎程式碼,進而標準化開發流程。作為一套專業且強大的 Monorepo 工具,其包含但不限於上述的功能,就讓我帶大家一探究竟 Nx 的強大吧!
應用程式(Application) 是 Nx Workspace 中最重要的角色,以微服務的角度來說,就會是各個服務,比如:用 NestJS 撰寫的訂單服務、用 Express 撰寫的金流服務等。由此可知,一個 Nx Workspace 可以有數十個甚至數百個 Application。
函式庫(Library) 與我們一般認知的函式庫不同,在 Nx 的世界裡,Library 是 模組化(Modularized) 程式碼的手段,並不完全是把程式碼共享化的方式。
一般來說,會鼓勵開發者們盡量將程式以 Library 的方式進行拆分,變成各個獨立的模組,並視情況進行 組合(Compose),最終於 Application 將 Library 組合成一個完整應用。如果不太理解上述的概念,我們可以將 Application 看作是一個應用程式的 容器(Container),而 Library 可以看作是應用程式的某個功能,會在 Container 將 Library 組合起來變成一個完整的應用。
注意:部分初學者會抗拒將非共用的程式碼切成 Library,但這樣其實 會讓 Nx 的效益打折,所以在使用 Nx 時,需要轉換思維,不要把 Library 只視為共享程式碼。至於為什麼會讓 Nx 的效益打折呢?這部分後面的篇章會做更詳細的說明。
在 Monorepo 的架構下,Application、Library 之間一定存在 依賴(Dependency) 關係,那麼要如何運用 Nx 知道它們之間的關聯呢?Nx 在建立 Application、Library 的時候,會有一個名為 project.json
的檔案,Nx 會偵測整個 Workspace 中有哪些 project.json
以識別該 Application、Library 的存在,再進一步根據程式碼、安裝的依賴項目來推定 Dependency,最終形成 Project Graph。下方是 project.json
的範例,可以看到 projectType
為 application
,表示這個專案是 Application,它的程式碼位置透過 sourceRoot
進行指向,並運用 name
替該專案命名:
{
"name": "todolist",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/todolist/src",
"projectType": "application",
"targets": {},
}
補充:Nx 為了避免頻繁偵測 Project Graph 導致效能問題,它會將 Project Graph 進行快取,並只針對變動的檔案進行分析。
雖然在大多數情況下 Nx 可以自動推斷出 Dependency,但有些 Dependency 可能會因為沒有程式碼上的相依關係導致推斷不出來,這時候可以透過手動的方式添加 Dependency。下方是 project.json
的範例,透過 implicitDependencies
標示 user
,表示該專案與 user
為 隱性依賴(Implicit Dependency) 關係:
{
"name": "todolist",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/todolist/src",
"projectType": "application",
"implicitDependencies": ["user"],
"targets": {}
}
Dependency 的關係會影響專案之間處理 任務(Task) 的先後順序,舉例來說,TodoList Application 依賴於 Todo 功能、Todo 功能依賴於 Utility Library,那麼要建置 TodoList 就必須先建置它的依賴項目 Todo,確保有元件可以使用,同樣的道理,在建置 Todo 之前必須先建置 Utility,如下圖所示:
在規模不大、依賴關係單純的情況下,要手動處理或是撰寫腳本來排序並執行特定 Task 並不是很麻煩的事情,但隨著規模擴展,這個問題將會變得越來越難處理,更不用說如果要 並行處理(Parallel) Task 同時滿足執行順序的情況。於是 Nx 設計了 Task Graph 來解決這個問題,基於 Project Graph 讓 Nx 推斷出 Task 的執行順序,並且找出可以 Parallel 的 Task。
那麼 Task 該如何設定呢?只需要在 project.json
中設定 target
區塊即可。下方是 project.json
的範例,可以看到 targets
設定了 serve
這個 Task,其中,使用了 @nx/js:node
這個 executor
作為 執行器(Executor):
{
"name": "todolist",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/todolist/src",
"projectType": "application",
"targets": {
"serve": {
"executor": "@nx/js:node",
// ...
}
}
}
補充:
executor
是用來決定這個 Task 該如何執行的重要角色,通常會與專案使用的 Stack 有關,以上方的範例來說,@nx/js:node
就是 Node.js 常用的 Executor。
雖然說 Task Graph 是基於 Project Graph 來推斷的,但一般情況下 Task Graph 並 不會 與 Project Graph 相同,可以用上方 TodoList 的例子來說明,TodoList 一定需要等待 Todo 建置完成,而 Todo 一定需要等待 Utility 建置完成;但 TodoList、Todo 與 Utility 執行單元測試可以不用有依賴關係,此時就可以 Parallel 處理,如下圖所示:
那麼 Task 之間的 Dependency 該如何建立呢?每個 Task 都可以透過 dependsOn
來決定在執行這個 Task 之前,有哪些 Dependency 應該優先處理。下方是 project.json
的範例,在 serve
這個 Task 執行之前,應該先執行自己專案的 build
Task:
{
"name": "todolist",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/todolist/src",
"projectType": "application",
"targets": {
"serve": {
"executor": "@nx/js:node",
"dependsOn": ["build"],
// ...
},
"build": {
// ...
}
}
}
另一種情況是需要先建置 Dependency 的專案,在寫法上需要做一些調整,將 dependsOn
內的 build
改成 ^build
,這樣 Nx 就知道要 先執行 Dependency 的 build
Task:
{
"name": "todolist",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/todolist/src",
"projectType": "application",
"targets": {
"serve": {
"executor": "@nx/js:node",
"dependsOn": ["^build", "build"],
// ...
}
}
}
有了 Nx 的基本知識後,就來實際建置 Nx Workspace。透過下方指令即可進行建置流程:
$ npx create-nx-workspace
此時 CLI 會詢問 Workspace 的名稱:
輸入完名稱之後,可以選擇預設建立的 APP 要用什麼 Stack,並安裝該 Stack 所需的相關套件、程式碼產生器。以我們的情境來說會選擇「NestJS」:
Nx 可以建置單一專案的 Standalone 或是 Integrated Monorepo,這邊我們會選擇「Integrated Monorepo」:
接著會詢問預設建立的 Application 名稱為何,這邊可以自由發揮:
Nx 很貼心地詢問是否要建立 Dockerfile
,由於只是簡單的 Demo,這裡選擇了「No」:
CI 的部分也會提供多樣選擇,由於只是簡單的 Demo,這裡選擇「Do it later」:
最後,會詢問要不要使用 雲端快取(Remote Caching),這塊是 Nx 獨有的功能,會將執行過的事情快取起來放到雲端,這樣所有專案的參與者都可以共享相同的 Cache。由於只是簡單的 Demo,這裡選擇「No」:
補充:建立 Workspace 的方式會因為版本不同而有些微變動。
以上步驟完成後,就會建置出一個 Workspace 囉,可以看到預設建立的 Application 在 apps
資料夾底下:
回顧一下今天所介紹的內容,在一開始講解了 PolyRepo 與 Monorepo 的優劣,再進一步聚焦在 Monorepo 的管理工具 - Nx。該工具解決了 Monorepo 管理、效能上的挑戰,基於 Project Graph 的機制,讓執行 Task 可以參照其路徑找出相關 Dependency,進而繪製出 Task Graph,讓龐大且複雜的 Task 流程變得更加容易管理與執行。最後,透過 create-nx-workspace
將 Nx Workspace 建置起來。
下一篇將會進一步介紹 Nx 相關的功能,敬請期待!